Passed
Pull Request — filestream (#169)
by
unknown
06:03
created

file-write.ts ➔ streamOriginalIntoNewFileAsync   A

Complexity

Conditions 3

Size

Total Lines 10
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 10
rs 9.95
c 0
b 0
f 0
cc 3
1
import {
2
    fsWritePromise,
3
    processFileSync,
4
    processFileAsync,
5
    fsExistsPromise,
6
    fsWriteFilePromise,
7
    fsRenamePromise,
8
    unlinkIfExistSync,
9
    unlinkIfExist,
10
    fsReadAsync
11
} from "./util-file"
12
import * as fs from 'fs'
13
import { Header, findId3TagPosition, getId3TagSize } from "./id3-tag"
14
import { WriteCallback, WriteOptions } from "./types/write"
15
import { hrtime } from "process"
16
17
// Must be at least Header.size which is the min size to detect an ID3 header.
18
// Naming it help identifying the code handling it.
19
const RolloverBufferSize = Header.size
20
21
const MinBufferSize = RolloverBufferSize + 1
22
const DefaultFileBufferSize = RolloverBufferSize + 20 * 1024 * 1024
23
24
export function writeId3TagToFileSync(
25
    filepath: string,
26
    id3Tag: Buffer,
27
    options: WriteOptions
28
): void {
29
    if (!fs.existsSync(filepath)) {
30
        fs.writeFileSync(filepath, id3Tag)
31
        return
32
    }
33
    const tempFilepath = makeTempFilepath(filepath)
34
    processFileSync(filepath, 'r', (readFileDescriptor) => {
35
        try {
36
            processFileSync(tempFilepath, 'w', (writeFileDescriptor) => {
37
                fs.writeSync(writeFileDescriptor, id3Tag)
38
                copyFileWithoutId3TagSync(
39
                    readFileDescriptor,
40
                    writeFileDescriptor,
41
                    getFileBufferSize(options)
42
                )
43
            })
44
        } catch(error) {
45
            unlinkIfExistSync(tempFilepath)
46
            throw error
47
        }
48
    })
49
    fs.renameSync(tempFilepath, filepath)
50
}
51
52
export function writeId3TagToFile(
53
    filepath: string,
54
    id3Tag: Buffer,
55
    options: WriteOptions,
56
    callback: WriteCallback
57
): void {
58
    writeId3TagToFileAsync(filepath, id3Tag, options)
59
    .then(
60
        () => callback(null),
61
        (error) => callback(error)
62
    )
63
}
64
65
export async function writeId3TagToFileAsync(
66
    filepath: string,
67
    id3Tag: Buffer,
68
    options: WriteOptions
69
): Promise<void> {
70
    if (!await fsExistsPromise(filepath)) {
71
        await fsWriteFilePromise(filepath, id3Tag)
72
        return
73
    }
74
    const tempFilepath = makeTempFilepath(filepath)
75
    await processFileAsync(filepath, 'r', async (readFileDescriptor) => {
76
        try {
77
            await processFileAsync(tempFilepath, 'w',
78
                async (writeFileDescriptor) => {
79
                    await fsWritePromise(writeFileDescriptor, id3Tag)
80
                    await copyFileWithoutId3TagAsync(
81
                        readFileDescriptor,
82
                        writeFileDescriptor,
83
                        getFileBufferSize(options)
84
                    )
85
                }
86
            )
87
        } catch(error) {
88
            await unlinkIfExist(tempFilepath)
89
            throw error
90
        }
91
92
    })
93
    await fsRenamePromise(tempFilepath, filepath)
94
}
95
96
function getFileBufferSize(options: WriteOptions) {
97
    return Math.max(
98
        options.fileBufferSize ?? DefaultFileBufferSize,
99
        MinBufferSize
100
    )
101
}
102
103
function makeTempFilepath(filepath: string) {
104
    // A high-resolution time is required to avoid potential conflicts
105
    // when running multiple tests in parallel for example.
106
    // Date.now() resolution is too low.
107
    return `${filepath}.tmp-${hrtime.bigint()}`
108
}
109
110
class Id3TagRemover {
111
    buffer: Buffer
112
    rolloverSize = 0
113
    continue = false
114
115
    constructor(bufferSize: number) {
116
        // TODO enfore min buffer size here,
117
        // i.e. bufferSize + RolloverBufferSize + 1
118
        this.buffer = Buffer.alloc(bufferSize)
119
    }
120
121
    getReadBuffer() {
122
        return this.buffer.subarray(this.rolloverSize)
123
    }
124
125
    processReadBuffer(readSize: number) {
126
        let data = this.buffer.subarray(0, this.rolloverSize + readSize)
127
128
        // Remove tags from `data`
129
        const parts: Buffer[] = []
130
        let missingBytes = 0
131
        let tag
132
        while((tag = getId3Tag(data))) {
133
            console.log(tag)
134
            parts.push(tag.before)
135
            data = tag.after
136
            missingBytes = tag.missingBytes
137
        }
138
139
        // Exclude rollover window on the last part
140
        this.rolloverSize = Math.min(RolloverBufferSize, data.length, readSize)
141
        const rolloverStart = data.length - this.rolloverSize
142
        const rolloverData = Buffer.from(data.subarray(rolloverStart))
143
        parts.push(data.subarray(0, rolloverStart))
144
145
        const writeBuffer = Buffer.concat(parts)
146
147
        // Update rollover window
148
        rolloverData.copy(this.buffer)
149
150
        this.continue = this.rolloverSize !==0 || missingBytes !== 0
151
152
        return {
153
            skipBuffer: Buffer.alloc(missingBytes),
154
            writeBuffer
155
        }
156
    }
157
}
158
159
function copyFileWithoutId3TagSync(
160
    readFileDescriptor: number,
161
    writeFileDescriptor: number,
162
    fileBufferSize: number
163
) {
164
    const remover = new Id3TagRemover(fileBufferSize)
165
    do {
166
        const readBuffer = remover.getReadBuffer()
167
        const sizeRead = fs.readSync(readFileDescriptor, readBuffer)
168
        const { skipBuffer, writeBuffer } = remover.processReadBuffer(sizeRead)
169
        fs.readSync(readFileDescriptor, skipBuffer)
170
        fs.writeSync(writeFileDescriptor, writeBuffer)
171
    } while(remover.continue)
172
}
173
174
async function copyFileWithoutId3TagAsync(
175
    readFileDescriptor: number,
176
    writeFileDescriptor: number,
177
    fileBufferSize: number
178
) {
179
    const remover = new Id3TagRemover(fileBufferSize)
180
    do {
181
        const readBuffer = remover.getReadBuffer()
182
        const sizeRead = await fsReadAsync(readFileDescriptor, readBuffer)
183
        const { skipBuffer, writeBuffer } = remover.processReadBuffer(sizeRead)
184
        await fsReadAsync(readFileDescriptor, skipBuffer)
185
        await fsWriteFilePromise(writeFileDescriptor, writeBuffer)
186
    } while(remover.continue)
187
}
188
189
function getId3Tag(data: Buffer) {
190
    const position = findId3TagPosition(data)
191
    if (position === -1) {
192
        return null
193
    }
194
    const from = data.subarray(position)
195
    const size = getId3TagSize(from)
196
    return {
197
        size,
198
        from,
199
        before: data.subarray(0, position),
200
        data: from.subarray(0, size),
201
        after: from.subarray(size),
202
        missingBytes: Math.max(0, size - from.length)
203
    }
204
}
205